/**
 * Copyright 2019 吉鼎科技.

 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.easyplatform.web.exporter.excel;

import cn.easyplatform.messages.vos.datalist.ListAuxHeadVo;
import cn.easyplatform.messages.vos.datalist.ListAuxHeaderVo;
import cn.easyplatform.messages.vos.datalist.ListHeaderVo;
import cn.easyplatform.messages.vos.datalist.ListVo;
import cn.easyplatform.web.exporter.AbstractExporter;
import cn.easyplatform.web.exporter.GroupRenderer;
import cn.easyplatform.web.exporter.RowRenderer;
import cn.easyplatform.web.exporter.excel.imp.CellValueSetterFactoryImpl;
import cn.easyplatform.web.exporter.util.Utils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.zkoss.zk.ui.Component;
import org.zkoss.zul.*;
import org.zkoss.zul.impl.HeaderElement;
import org.zkoss.zul.impl.MeshElement;

import java.io.IOException;
import java.io.OutputStream;
import java.util.*;

import static cn.easyplatform.web.exporter.util.Utils.*;

/**
 * @author <a href="mailto:davidchen@epclouds.com">littleDog</a> <br/>
 * @since 2.0.0 <br/>
 */
public class ExcelExporter extends AbstractExporter<XSSFWorkbook, Row> {

    private ExportContext _exportContext;
    private CellValueSetterFactory _cellValueSetterFactory;
    private XSSFWorkbook book;
    private Map<HorizontalAlignment, XSSFCellStyle> cellStyleMap = new HashMap<>(3);

    private XSSFCellStyle getCellStyle(HorizontalAlignment alignment) {
        XSSFCellStyle style = cellStyleMap.get(alignment);
        if (style == null)
            cellStyleMap.put(alignment, style = book.createCellStyle());
        return style;
    }

    public <D> void export(int columnSize, Collection<D> data,
                           RowRenderer<Row, D> renderer, OutputStream outputStream)
            throws IOException {
        book = new XSSFWorkbook();
        ExportContext ctx = new ExportContext(true, book.createSheet("Sheet1"));
        XSSFSheet sheet = ctx.getSheet();
        setExportContext(ctx);

        if (getInterceptor() != null)
            getInterceptor().beforeRendering(book);
        int rowIndex = 0;
        for (D d : data) {
            renderer.render(getOrCreateRow(ctx.moveToNextRow(), sheet), d,
                    (rowIndex++ % 2 == 1));
        }
        if (getInterceptor() != null)
            getInterceptor().afterRendering(book);

        adjustColumnWidth(columnSize);

        book.write(outputStream);
        setExportContext(null);
    }

    public <D> void export(int columnSize, Collection<Collection<D>> data,
                           GroupRenderer<Row, D> renderer, OutputStream outputStream)
            throws IOException {
        book = new XSSFWorkbook();
        ExportContext ctx = new ExportContext(true, book.createSheet("Sheet1"));
        XSSFSheet sheet = ctx.getSheet();
        setExportContext(ctx);

        if (getInterceptor() != null)
            getInterceptor().beforeRendering(book);

        int rowIndex = 0;
        for (Collection<D> group : data) {
            renderer.renderGroup(getOrCreateRow(ctx.moveToNextRow(), sheet),
                    group);
            for (D d : group) {
                renderer.render(getOrCreateRow(ctx.moveToNextRow(), sheet), d,
                        (rowIndex++ % 2 == 1));
            }
            renderer.renderGroupfoot(
                    getOrCreateRow(ctx.moveToNextRow(), sheet), group);
        }

        if (getInterceptor() != null)
            getInterceptor().afterRendering(book);

        adjustColumnWidth(columnSize);
        book.write(outputStream);
        setExportContext(null);
    }

    public void setExportContext(ExportContext ctx) {
        _exportContext = ctx;
    }

    public ExportContext getExportContext() {
        return _exportContext;
    }

    public void setCellValueSetterFactory(
            CellValueSetterFactory cellValueSetterFactory) {
        _cellValueSetterFactory = cellValueSetterFactory;
    }

    public CellValueSetterFactory getCellValueSetterFactory() {
        if (_cellValueSetterFactory == null) {
            _cellValueSetterFactory = new CellValueSetterFactoryImpl();
        }
        return _cellValueSetterFactory;
    }

    private void adjustColumnWidth(int columnSize) {
        XSSFSheet sheet = getExportContext().getSheet();
        for (int c = 0; c < columnSize; c++) {
            sheet.autoSizeColumn(c);
        }
    }

    @Override
    protected void exportTabularComponent(ListVo entity,
                                          MeshElement component, OutputStream outputStream) throws Exception {
        book = new XSSFWorkbook();
        setExportContext(new ExportContext(true, book.createSheet("Sheet1")));

        int columnSize = getHeaderSize(component);
        exportHeaders(entity, columnSize, component, book);
        exportRows(entity.getHeaders(), columnSize, component, book);
        exportFooters(entity.getHeaders(), columnSize, component, book);

        adjustColumnWidth(columnSize);

        book.write(outputStream);
        setExportContext(null);
    }


    @Override
    protected void exportTabularComponent(MeshElement component, OutputStream outputStream) throws Exception {
        book = new XSSFWorkbook();
        setExportContext(new ExportContext(true, book.createSheet("Sheet1")));

        int columnSize = getHeaderSize(component);
        exportHeaders(columnSize, component, book);
        exportRows(columnSize, component, book);
        exportFooters(columnSize, component, book);

        adjustColumnWidth(columnSize);

        book.write(outputStream);
        setExportContext(null);
    }

    @Override
    protected void exportAuxhead(int columnSize, Auxhead auxhead, XSSFWorkbook book) {
        //TODO: process row span
        exportCellsWithSpan(columnSize, auxhead, book);
    }

    /**
     * 导出表头
     *
     * @param columnSize
     * @param target
     * @param book
     */
    protected void exportHeaders(ListVo entity, int columnSize,
                                 MeshElement target, XSSFWorkbook book) {
        super.exportHeaders(entity, columnSize, target, book);
    }

    @Override
    protected void exportAuxhead(ListVo entity,
                                 int columnSize, Auxhead auxhead, XSSFWorkbook book) {
        ExportContext ctx = getExportContext();
        XSSFSheet sheet = ctx.getSheet();
        boolean showRowNo = entity.getHeaders().get(0).getName().equals("LIST_INDEX");
        if (showRowNo) {
            boolean visible = true;
            if (auxhead.getParent() instanceof Listbox) {
                visible = ((Listbox) auxhead.getParent()).getListhead().isVisible();
            } else if (auxhead.getParent() instanceof Grid) {
                visible = ((Grid) auxhead.getParent()).getColumns().isVisible();
            } else if (auxhead.getParent() instanceof Tree) {
                visible = ((Tree) auxhead.getParent()).getTreecols().isVisible();
            }
            sheet.addMergedRegion(new CellRangeAddress(0, visible ? entity.getAuxHeads().size() : entity.getAuxHeads().size() - 1, 0, 0));
            getOrCreateCell(ctx.moveToNextCell(), sheet).setCellValue(entity.getHeaders().get(0).getTitle());
        }
        if (entity.getAuxHeads() != null) {
            Map<Integer, ListAuxHeaderVo> colspan = new HashMap<>();
            for (ListAuxHeadVo head : entity.getAuxHeads()) {
                int offset = showRowNo ? 1 : 0;
                int idx = 0;
                int count = 0;
                for (ListAuxHeaderVo header : head.getAuxHeaders()) {
                    Cell cell = null;
                    if (header.getColspan() > 1 || header.getRowspan() > 1) {
                        sheet.addMergedRegion(new CellRangeAddress(ctx.getRowIndex(), ctx.getRowIndex() + header.getRowspan() - 1, offset, offset + header.getColspan() - 1));
                        cell = getOrCreateCell(ctx.getRowIndex(), offset, sheet);
                        if (header.getColspan() > 1) {
                            header.setColumnIndex(offset);
                            colspan.put(idx++, header);
                        }
                        offset += header.getColspan() - 1;
                    } else {
                        if (colspan.containsKey(idx)) {
                            ListAuxHeaderVo h = colspan.get(idx);
                            if (count < h.getColspan()) {
                                offset = h.getColumnIndex() + count;
                                count++;
                            } else {
                                idx++;
                                if (colspan.containsKey(idx)) {
                                    h = colspan.get(idx);
                                    offset = h.getColumnIndex();
                                    count = 1;
                                }
                            }
                        }
                        cell = getOrCreateCell(ctx.getRowIndex(), offset, sheet);
                    }
                    cell.setCellValue(header.getTitle());
                    final String align = header.getAlign();
                    if ("center".equals(align)) {
                        setCellAlignment(HorizontalAlignment.CENTER, cell, book);
                    } else if ("right".equals(align)) {
                        setCellAlignment(HorizontalAlignment.RIGHT, cell, book);
                    } else if ("left".equals(align)) {
                        setCellAlignment(HorizontalAlignment.LEFT, cell, book);
                    }
                    offset++;
                }
                ctx.moveToNextRow();
            }
        } else
            exportCellsWithSpan(columnSize, auxhead, book);
    }

    private void setCellAlignment(HorizontalAlignment alignment, Cell cell, XSSFWorkbook book) {
        if (cell.getCellStyle().getAlignment() != alignment) {
            //book.createCellStyle();如果直接调用会导致The maximum number of cell styles was exceeded. You can define up to 4000 styles in a .xls workbook
            XSSFCellStyle cellStyle = getCellStyle(alignment);
            cellStyle.cloneStyleFrom(cell.getCellStyle());
            cellStyle.setAlignment(alignment);
            cell.setCellStyle(cellStyle);
        }
    }

    private boolean syncAlignment(Component cmp, Cell cell, XSSFWorkbook book) {
        if (cmp == null)
            return false;

        final String align = getAlign(cmp);
        if ("center".equals(align)) {
            setCellAlignment(HorizontalAlignment.CENTER, cell, book);
            return true;
        } else if ("right".equals(align)) {
            setCellAlignment(HorizontalAlignment.RIGHT, cell, book);
            return true;
        } else if ("left".equals(align)) {
            setCellAlignment(HorizontalAlignment.LEFT, cell, book);
            return true;
        }
        return false;
    }

    private void syncAlignment(Component cmp, Component header, Cell cell,
                               XSSFWorkbook book) {
        // check if component define align, if not check header's align
        if (!syncAlignment(cmp, cell, book) && header != null) {
            syncAlignment(header, cell, book);
        }
    }

    @Override
    protected void exportColumnHeaders(Component component, XSSFWorkbook book) {
        CellValueSetter<Component> cellValueSetter = getCellValueSetterFactory().getCellValueSetter(Component.class);
        ExportContext ctx = getExportContext();
        XSSFSheet sheet = ctx.getSheet();
        for (Component c : component.getChildren()) {

            Cell cell = getOrCreateCell(ctx.moveToNextCell(), sheet);
            cellValueSetter.setCellValue(c, cell);
            syncAlignment(c, cell, book);
        }
        ctx.moveToNextRow();
    }

    @Override
    protected void exportColumnHeaders(List<ListHeaderVo> headers,
                                       Component component, XSSFWorkbook book) {
        CellValueSetter<Component> cellValueSetter = getCellValueSetterFactory()
                .getCellValueSetter(Component.class);
        ExportContext ctx = getExportContext();
        XSSFSheet sheet = ctx.getSheet();
        int index = 0;
        for (Component c : component.getChildren()) {
            if (index < headers.size()) {
                ListHeaderVo listheader = headers.get(index++);
                if (listheader.isExport()) {
                    Cell cell = getOrCreateCell(ctx.moveToNextCell(), sheet);
                    cellValueSetter.setCellValue(c, cell);
                    syncAlignment(c, cell, book);
                }
            }
        }
        ctx.moveToNextRow();
    }

    @Override
    protected void exportGroup(int columnSize,
                               Component group, XSSFWorkbook book) {
        Iterator<Component> iterator = group.getChildren().iterator();

        CellValueSetter<Component> cellValueSetter = getCellValueSetterFactory()
                .getCellValueSetter(Component.class);
        ExportContext context = getExportContext();
        XSSFSheet sheet = context.getSheet();
        while (iterator.hasNext()) {
            Component cmp = iterator.next();

            Cell cell = getOrCreateCell(context.moveToNextCell(), sheet);
            cellValueSetter.setCellValue(cmp, cell);
        }
        context.moveToNextRow();
    }

    @Override
    protected void exportGroupfoot(int columnSize, Component groupfoot, XSSFWorkbook book) {
        exportCellsWithSpan(columnSize, groupfoot, book);
    }

    private String indent(int level) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < level; i++)
            sb.append("  ");
        return sb.toString();
    }

    @Override
    protected void exportCells(int rowIndex, int columnSize, Component row, XSSFWorkbook book) {
        CellValueSetter<Component> cellValueSetter = getCellValueSetterFactory().getCellValueSetter(Component.class);
        ExportContext ctx = getExportContext();
        XSSFSheet sheet = ctx.getSheet();

        HashMap<Integer, Component> headers = buildHeaderIndexMap(getHeaders(getTarget(row)));
        List<Component> children = row.getChildren();
        for (int c = 0; c < columnSize; c++) {
            Component cmp = c < children.size() ? children.get(c) : null;

            if (cmp == null) {
                return;
            }

            Cell cell = getOrCreateCell(ctx.moveToNextCell(), sheet);
            cellValueSetter.setCellValue(cmp, cell);

            syncAlignment(cmp, headers != null ? headers.get(c) : null, cell, book);
        }
        ctx.moveToNextRow();
    }

    @Override
    protected void exportCells(List<ListHeaderVo> listheaders, int rowIndex,
                               int columnSize, Component row, XSSFWorkbook book) {
        CellValueSetter<Component> cellValueSetter = getCellValueSetterFactory()
                .getCellValueSetter(Component.class);
        ExportContext ctx = getExportContext();
        XSSFSheet sheet = ctx.getSheet();

        HashMap<Integer, Component> headers = buildHeaderIndexMap(getHeaders(getTarget(row)));
        List<Component> children = row.getChildren();
        for (int c = 0; c < columnSize; c++) {
            Component cmp = c < children.size() ? children.get(c) : null;
            if (cmp == null)
                return;
            if (c < listheaders.size()) {
                ListHeaderVo listheader = listheaders.get(c);
                if (listheader.isExport()) {
                    Cell cell = getOrCreateCell(ctx.moveToNextCell(), sheet);
                    if (!(cmp instanceof Treecell) || c != 0)
                        cellValueSetter.setCellValue(listheader, cmp, cell);
                    else {
                        Treeitem item = (Treeitem) cmp.getParent().getParent();

                        String label = indent(item.getLevel())
                                + Utils.getStringValue(cmp);
                        cell.setCellValue(label);
                    }

                    syncAlignment(cmp, headers != null ? headers.get(c) : null,
                            cell, book);
                }
            }
        }
        ctx.moveToNextRow();
    }

    @Override
    protected void exportFooters(int columnSize, Component target, XSSFWorkbook book) {
        Component footers = getFooters(target);
        if (footers == null) {
            return;
        }
        exportCellsWithSpan(columnSize, footers, book);
    }

    @Override
    protected void exportFooters(List<ListHeaderVo> headers, int columnSize,
                                 Component target, XSSFWorkbook book) {
        Component footers = getFooters(target);
        if (footers == null) {
            return;
        }
        exportCellsWithSpan(columnSize, footers, book);
    }

    private void exportCellsWithSpan(int columnSize, Component component, XSSFWorkbook book) {
        ExportContext ctx = getExportContext();
        XSSFSheet sheet = ctx.getSheet();
        for (Component cmp : component.getChildren()) {
            int span = getColSpan(cmp);
            if (span == 1) {
                getOrCreateCell(ctx.moveToNextCell(), sheet).setCellValue(
                        getStringValue(cmp));
            } else {
                // TODO: merge col span
                // TODO: not tested yet
                int colIdx = ctx.getColumnIndex() + span;
                ctx.setColumnIndex(colIdx);
                getOrCreateCell(ctx.getRowIndex(), colIdx, sheet).setCellValue(
                        getStringValue(cmp));
            }
        }
        ctx.moveToNextRow();
    }

    private HashMap<Integer, Component> buildHeaderIndexMap(Component target) {
        if (target == null)
            return null;

        HashMap<Integer, Component> headers = new HashMap<Integer, Component>();

        int idx = 0;
        for (Component c : target.getChildren()) {
            if (!(c instanceof HeaderElement)) {
                throw new IllegalArgumentException(c
                        + " is not type of HeaderElement");
            }
            headers.put(idx++, c);
        }

        return headers;
    }

    public static Row getOrCreateRow(int[] idx, Sheet sheet) {
        return getOrCreateRow(idx[0], sheet);
    }

    public static Row getOrCreateRow(int row, Sheet sheet) {
        Row r = sheet.getRow(row);
        if (r == null) {
            return sheet.createRow(row);
        }
        return r;
    }

    private static int getColSpan(Component cmp) {
        int span = 1;
        Object spanVal = invokeComponentGetter(cmp, "getColspan", "getSpan");
        if (spanVal != null && spanVal instanceof Number)
            span = ((Number) spanVal).intValue();
        return span;
    }

    private static int getRowSpan(Component cmp) {
        int span = 1;
        Object spanVal = invokeComponentGetter(cmp, "getRowspan");
        if (spanVal != null && spanVal instanceof Number)
            span = ((Number) spanVal).intValue();
        return span;
    }

    public static Cell getOrCreateCell(int[] idx, Sheet sheet) {
        return getOrCreateCell(idx[0], idx[1], sheet);
    }

    public static Cell getOrCreateCell(int row, int col, Sheet sheet) {
        Row r = getOrCreateRow(row, sheet);
        Cell cell = r.getCell(col);
        if (cell == null) {
            return r.createCell(col);
        }
        return cell;
    }

    public static class ExportContext {
        int _rowIndex = 0;
        int _columnIndex = -1;
        final XSSFSheet _sheet;
        final boolean _exportByComponentReference;

        ExportContext(boolean isExportByComponentReference, XSSFSheet worksheet) {
            _exportByComponentReference = isExportByComponentReference;
            _sheet = worksheet;
        }

        public boolean isExportByComponentReference() {
            return _exportByComponentReference;
        }

        public void setRowIndex(int rowIndex) {
            _rowIndex = rowIndex;
        }

        public int getRowIndex() {
            return _rowIndex;
        }

        public void setColumnIndex(int columnIndex) {
            _columnIndex = columnIndex;
        }

        public int getColumnIndex() {
            return _columnIndex;
        }

        public int[] moveToNextCell() {
            return new int[]{_rowIndex < 0 ? _rowIndex = 0 : _rowIndex,
                    _columnIndex < 0 ? _columnIndex = 0 : ++_columnIndex};
        }

        public int[] moveToNextRow() {
            return new int[]{++_rowIndex, _columnIndex = -1};
        }

        public XSSFSheet getSheet() {
            return _sheet;
        }
    }

    // TODO: not tested yet
    @Override
    public <D> void export(String[] columnHeaders, Collection<D> data,
                           RowRenderer<Row, D> renderer, OutputStream outputStream)
            throws Exception {
        final int columnSize = columnHeaders.length;

        // TODO: need to log if not ExportColumnHeaderInterceptorImpl ?
        if (getInterceptor() == null)
            setInterceptor(new ExportColumnHeaderInterceptorImpl(columnHeaders));
        export(columnSize, data, renderer, outputStream);
    }

    // TODO: not tested yet
    @Override
    public <D> void export(String[] columnHeaders,
                           Collection<Collection<D>> data, GroupRenderer<Row, D> renderer,
                           OutputStream outputStream) throws Exception {

        if (getInterceptor() == null)
            setInterceptor(new ExportColumnHeaderInterceptorImpl(columnHeaders));

        export(columnHeaders, data, renderer, outputStream);
    }

    // export header
    private class ExportColumnHeaderInterceptorImpl implements
            cn.easyplatform.web.exporter.Interceptor<XSSFWorkbook> {

        private final String[] _columnHeaders;

        public ExportColumnHeaderInterceptorImpl(String[] columnHeaders) {
            _columnHeaders = columnHeaders;
        }

        @Override
        public void beforeRendering(XSSFWorkbook book) {
            int columnSize = _columnHeaders.length;
            boolean renderHeader = false;
            for (int i = 0; i < columnSize; i++) {
                String e = _columnHeaders[i];
                if (e != null && e.length() > 0) {
                    renderHeader = true;
                    break;
                }
            }
            if (renderHeader) {
                ExportContext ctx = getExportContext();
                XSSFSheet sheet = ctx.getSheet();

                for (String header : _columnHeaders) {
                    getOrCreateCell(ctx.moveToNextCell(), sheet).setCellValue(
                            header);
                }
            }
        }

        @Override
        public void afterRendering(XSSFWorkbook book) {
        }
    }
}